﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Animations;
using System.Drawing;
using System.Diagnostics;

namespace Silvermoon.Controls
{
    public class ListViewItemAnimator
    {
        #region classes

        /// <summary>
        /// helper class to group properties.
        /// </summary>
        private class ItemDelta
        {
            public int Delta;
            internal int ItemOffsetIndex;
        }

        /// <summary>
        /// helper class that holds offsets to apply.
        /// </summary>
        private class ItemOffset
        {
            const int startDelay = 250; // delay 250 ms.
            public int ItemIndex;
            public int Delta;
            private int startDelta;
            private int startTick;
            const float duration = 300F;

            public ScaleTransform Transformation { get; private set; }

            public ItemOffset(int itemIndex, int delta)
            {
                this.ItemIndex = itemIndex;
                this.Delta = delta;
                this.startDelta = delta;
                if (delta < 0)
                {
                    //Transformation = new RotateTransform { Axis = RotateAxis.XAxis, Angle = 90 };
                    Transformation = new ScaleTransform();
                }
                startTick = Environment.TickCount + startDelay;
            }

            /// <summary>
            /// Processes the animation on time.
            /// </summary>
            /// <param name="tick">time tick.</param>
            /// <returns>true if processing is completed, otherwise false.</returns>
            public bool Process(int tick)
            {
                float t = Math.Max(0f, (float)(tick - startTick) / duration);

                bool result = t >= 1f;
                if (result) t = 1f;
                IEasingFunction ease = PowerEase.QuadraticOut;
                float v = 1f - ease.Ease(t);
                Delta = (int)(v * startDelta);
                if (startDelta < 0)
                {
                    //Transformation.Angle = 90f * v;
                    Transformation.Y = 1f - v;
                }
                return result;
            }

            /// <summary>
            /// Restarts processing of animation.
            /// </summary>
            public void Restart()
            {
                startDelta = Delta + startDelay;
                startTick = Environment.TickCount;
            }
        }

        #endregion

        private ItemDelta delta = new ItemDelta();
        private List<ItemOffset> offsets = new List<ItemOffset>();

        /// <summary>
        /// Gets whether the animator has data to animate.
        /// </summary>
        public bool IsAnimated { get; private set; }

        /// <summary>
        /// Gets the animated offset at the specified index.
        /// </summary>
        /// <param name="index">item index.</param>
        /// <returns>offset at the specified index. </returns>
        public virtual int GetIndexOffset(int index)
        {
            ProcessAnimationState();
            delta.Delta = 0;
            delta.ItemOffsetIndex = -0;
            int d = 0;
            for (int i = 0; i < offsets.Count; i++)
            {
                var offset = offsets[i];
                if (offset.ItemIndex > index) break;
                delta.ItemOffsetIndex = i;
                d += offset.Delta;
            }
            return d;
        }

        /// <summary>
        /// Adjusts the control due to animation state.
        /// This usually changes the Location of the control.
        /// </summary>
        /// <param name="item">Item to adjust</param>
        /// <param name="index">index of the item.</param>
        public virtual void AdjustNextItem(Control item, int index)
        {
            while (offsets.Count > delta.ItemOffsetIndex)
            {
                ItemOffset offset = offsets[delta.ItemOffsetIndex];
                int di = offset.ItemIndex - index;
                Transformation t = offset.Transformation;
                if (t != null) item.Transformation -= t;
                if (t!=null && di == 0 && offset.Delta < 0)
                {
                    item.Transformation += t;
                }
                if (di > 0) break;
                delta.Delta += offset.Delta;
                delta.ItemOffsetIndex++;
            }
            item.Y += delta.Delta;
        }

        /// <summary>
        /// Performs a quick binary search for less or equal.
        /// </summary>
        private int BinarySearchLeEq(int index)
        {
            int max = offsets.Count;
            if (max == 0) return 0;
            int min = 0;
            int i = max >> 1;

            while (min < max)
            {
                var item = offsets[i];
                int dif = item.ItemIndex - index;

                if (dif == 0) return i;
                if (dif < 0) min = i + 1;
                else max = i;
                i = (min + max) >> 1;
            }
            return i - 1;
        }

        /// <summary>
        /// Adds a new offset for a previously removed item.
        /// </summary>
        /// <param name="index">index of the removed item.</param>
        /// <param name="height">height of the item.</param>
        public virtual void ItemRemoved(int index, int height)
        {
            int n = offsets.Count;
            ItemOffset offset = null;
            int offsetIndex = 0;
            int di = height > 0 ? -1 : 1;
            if (n > 0)
            {
                offsetIndex = BinarySearchLeEq(index);
                if (offsetIndex >= 0 && offsets[offsetIndex].ItemIndex == index)
                {
                    if (height > 0 && offsets[offsetIndex].Delta > 0)
                    {
                        offset = offsets[offsetIndex];
                        offset.Delta += height;
                        offset.Restart();
                    }
                }
                else offsetIndex++;
            }
            if (offset == null)
            {
                offset = new ItemOffset(index, height);
                offsets.Insert(offsetIndex, offset);
            }

            offsetIndex = CombineOffsetNearby(offset, offsetIndex);
            if (offset.Delta == 0)
            {
                offsets.RemoveAt(offsetIndex);
                offsetIndex--;
                if (offsetIndex < 0) offsetIndex = 0;
            }

            n = offsets.Count;
            IsAnimated = n > 0;
            for (int i = offsetIndex + 1; i < n; i++) offsets[i].ItemIndex += di;
        }

        /// <summary>
        /// Adds a new offset for a newly added item.
        /// </summary>
        /// <param name="index">index of the new item.</param>
        /// <param name="height">height of the item.</param>
        public virtual void ItemInserted(int index, int height)
        {
            ItemRemoved(index, -height);
        }

        /// <summary>
        /// Combines the previous and/or next ItemOffset with the given one, if possible.
        /// </summary>
        /// <param name="offset">ItemOffset to combine.</param>
        /// <param name="offsetIndex">Index of the given. ItemOffset.</param>
        /// <returns>Modified offsetIndex.</returns>
        private int CombineOffsetNearby(ItemOffset offset, int offsetIndex)
        {
            if (offsetIndex > 0 && offset.Delta > 0)
            {
                ItemOffset prev = offsets[offsetIndex - 1];
                if (prev.Delta > 0 && prev.ItemIndex == offsetIndex)
                {
                    offset.Delta += prev.Delta;
                    offsetIndex--;
                    offsets.RemoveAt(offsetIndex);
                    offset.Restart();
                }
            }
            if (offsetIndex < offsets.Count - 1)
            {
                ItemOffset next = offsets[offsetIndex + 1];
                if (next.Delta > 0 && next.ItemIndex == offsetIndex + 1)
                {
                    offset.Delta += next.Delta;
                    offsets.RemoveAt(offsetIndex + 1);
                    offset.Restart();
                }
            }

            return offsetIndex;
        }

        /// <summary>
        /// Processes the state of the animation on time.
        /// </summary>
        public virtual void ProcessAnimationState()
        {
            int n = offsets.Count;
            if (n > 0)
            {
                int tick = Environment.TickCount;
                for (int i = 0; i < n; i++)
                {
                    ItemOffset offset = offsets[i];
                    if (offset.Process(tick))
                    {
                        offsets.RemoveAt(i);
                        i--;
                        n--;
                    }
                }
            }
            IsAnimated = n > 0;
        }


        /// <summary>
        /// Gets the total offset.
        /// </summary>
        /// <returns>total offset.</returns>
        public virtual int GetTotalOffset()
        {
            int h = 0;
            foreach (ItemOffset offset in offsets)
            {
                h += offset.Delta;
            }
            return h;
        }
    }
}